// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Global

/* XUnit.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Diagnostics;
using System.Globalization;

#endregion

#nullable enable

namespace PdfSharpCore.Drawing;

/// <summary>
/// Represents a value and its unit of measure. The structure converts implicitly from and to
/// double with a value measured in point.
/// </summary>
[DebuggerDisplay ("{DebuggerDisplay}")]
public struct XUnit
    : IFormattable
{
    internal const double PointFactor = 1;
    internal const double InchFactor = 72;
    internal const double MillimeterFactor = 72 / 25.4;
    internal const double CentimeterFactor = 72 / 2.54;
    internal const double PresentationFactor = 72 / 96.0;

    internal const double PointFactorWpf = 96 / 72.0;
    internal const double InchFactorWpf = 96;
    internal const double MillimeterFactorWpf = 96 / 25.4;
    internal const double CentimeterFactorWpf = 96 / 2.54;
    internal const double PresentationFactorWpf = 1;

    /// <summary>
    /// Initializes a new instance of the XUnit class with type set to point.
    /// </summary>
    public XUnit (double point)
    {
        _value = point;
        _type = XGraphicsUnit.Point;
    }

    /// <summary>
    /// Initializes a new instance of the XUnit class.
    /// </summary>
    public XUnit (double value, XGraphicsUnit type)
    {
        if (!Enum.IsDefined (typeof (XGraphicsUnit), type))
        {
            throw new ArgumentException ("type");
        }

        _value = value;
        _type = type;
    }

    /// <summary>
    /// Gets the raw value of the object without any conversion.
    /// To determine the XGraphicsUnit use property <code>Type</code>.
    /// To get the value in point use the implicit conversion to double.
    /// </summary>
    public double Value => _value;

    /// <summary>
    /// Gets the unit of measure.
    /// </summary>
    public XGraphicsUnit Type => _type;

    /// <summary>
    /// Gets or sets the value in point.
    /// </summary>
    public double Point
    {
        get
        {
            return _type switch
            {
                XGraphicsUnit.Point => _value,
                XGraphicsUnit.Inch => _value * 72,
                XGraphicsUnit.Millimeter => _value * 72 / 25.4,
                XGraphicsUnit.Centimeter => _value * 72 / 2.54,
                XGraphicsUnit.Presentation => _value * 72 / 96,
                _ => throw new InvalidCastException()
            };
        }
        set
        {
            _value = value;
            _type = XGraphicsUnit.Point;
        }
    }

    /// <summary>
    /// Gets or sets the value in inch.
    /// </summary>
    public double Inch
    {
        get
        {
            return _type switch
            {
                XGraphicsUnit.Point => _value / 72,
                XGraphicsUnit.Inch => _value,
                XGraphicsUnit.Millimeter => _value / 25.4,
                XGraphicsUnit.Centimeter => _value / 2.54,
                XGraphicsUnit.Presentation => _value / 96,
                _ => throw new InvalidCastException()
            };
        }
        set
        {
            _value = value;
            _type = XGraphicsUnit.Inch;
        }
    }

    /// <summary>
    /// Gets or sets the value in millimeter.
    /// </summary>
    public double Millimeter
    {
        get
        {
            return _type switch
            {
                XGraphicsUnit.Point => _value * 25.4 / 72,
                XGraphicsUnit.Inch => _value * 25.4,
                XGraphicsUnit.Millimeter => _value,
                XGraphicsUnit.Centimeter => _value * 10,
                XGraphicsUnit.Presentation => _value * 25.4 / 96,
                _ => throw new InvalidCastException()
            };
        }
        set
        {
            _value = value;
            _type = XGraphicsUnit.Millimeter;
        }
    }

    /// <summary>
    /// Gets or sets the value in centimeter.
    /// </summary>
    public double Centimeter
    {
        get
        {
            return _type switch
            {
                XGraphicsUnit.Point => _value * 2.54 / 72,
                XGraphicsUnit.Inch => _value * 2.54,
                XGraphicsUnit.Millimeter => _value / 10,
                XGraphicsUnit.Centimeter => _value,
                XGraphicsUnit.Presentation => _value * 2.54 / 96,
                _ => throw new InvalidCastException()
            };
        }
        set
        {
            _value = value;
            _type = XGraphicsUnit.Centimeter;
        }
    }

    /// <summary>
    /// Gets or sets the value in presentation units (1/96 inch).
    /// </summary>
    public double Presentation
    {
        get
        {
            return _type switch
            {
                XGraphicsUnit.Point => _value * 96 / 72,
                XGraphicsUnit.Inch => _value * 96,
                XGraphicsUnit.Millimeter => _value * 96 / 25.4,
                XGraphicsUnit.Centimeter => _value * 96 / 2.54,
                XGraphicsUnit.Presentation => _value,
                _ => throw new InvalidCastException()
            };
        }
        set
        {
            _value = value;
            _type = XGraphicsUnit.Point;
        }
    }

    /// <summary>
    /// Returns the object as string using the format information.
    /// The unit of measure is appended to the end of the string.
    /// </summary>
    public string ToString (IFormatProvider formatProvider)
    {
        var valuestring = _value.ToString (formatProvider) + GetSuffix();
        return valuestring;
    }

    /// <summary>
    /// Returns the object as string using the specified format and format information.
    /// The unit of measure is appended to the end of the string.
    /// </summary>
    string IFormattable.ToString (string? format, IFormatProvider? formatProvider)
    {
        var valuestring = _value.ToString (format, formatProvider) + GetSuffix();
        return valuestring;
    }

    /// <summary>
    /// Returns the object as string. The unit of measure is appended to the end of the string.
    /// </summary>
    public override string ToString()
    {
        var valuestring = _value.ToString (CultureInfo.InvariantCulture) + GetSuffix();
        return valuestring;
    }

    /// <summary>
    /// Returns the unit of measure of the object as a string like 'pt', 'cm', or 'in'.
    /// </summary>
    string GetSuffix()
    {
        return _type switch
        {
            XGraphicsUnit.Point => "pt",
            XGraphicsUnit.Inch => "in",
            XGraphicsUnit.Millimeter => "mm",
            XGraphicsUnit.Centimeter => "cm",
            XGraphicsUnit.Presentation => "pu",
            _ => throw new InvalidCastException()
        };
    }

    /// <summary>
    /// Returns an XUnit object. Sets type to point.
    /// </summary>
    public static XUnit FromPoint (double value)
    {
        XUnit unit;
        unit._value = value;
        unit._type = XGraphicsUnit.Point;
        return unit;
    }

    /// <summary>
    /// Returns an XUnit object. Sets type to inch.
    /// </summary>
    public static XUnit FromInch (double value)
    {
        XUnit unit;
        unit._value = value;
        unit._type = XGraphicsUnit.Inch;
        return unit;
    }

    /// <summary>
    /// Returns an XUnit object. Sets type to millimeters.
    /// </summary>
    public static XUnit FromMillimeter (double value)
    {
        XUnit unit;
        unit._value = value;
        unit._type = XGraphicsUnit.Millimeter;
        return unit;
    }

    /// <summary>
    /// Returns an XUnit object. Sets type to centimeters.
    /// </summary>
    public static XUnit FromCentimeter (double value)
    {
        XUnit unit;
        unit._value = value;
        unit._type = XGraphicsUnit.Centimeter;
        return unit;
    }

    /// <summary>
    /// Returns an XUnit object. Sets type to Presentation.
    /// </summary>
    public static XUnit FromPresentation (double value)
    {
        XUnit unit;
        unit._value = value;
        unit._type = XGraphicsUnit.Presentation;
        return unit;
    }

    /// <summary>
    /// Converts a string to an XUnit object.
    /// If the string contains a suffix like 'cm' or 'in' the object will be converted
    /// to the appropriate type, otherwise point is assumed.
    /// </summary>
    public static implicit operator XUnit (string value)
    {
        XUnit unit;
        value = value.Trim();

        // HACK for Germans...
        value = value.Replace (',', '.');

        var count = value.Length;
        var valLen = 0;
        for (; valLen < count;)
        {
            var ch = value[valLen];
            if (ch == '.' || ch == '-' || ch == '+' || char.IsNumber (ch))
            {
                valLen++;
            }
            else
            {
                break;
            }
        }

        try
        {
            unit._value = double.Parse (value.Substring (0, valLen).Trim(), CultureInfo.InvariantCulture);
        }
        catch (Exception ex)
        {
            unit._value = 1;
            var message = $"String '{value}' is not a valid value for structure 'XUnit'.";
            throw new ArgumentException (message, ex);
        }

        var typeStr = value.Substring (valLen).Trim().ToLower();
        unit._type = XGraphicsUnit.Point;
        switch (typeStr)
        {
            case "cm":
                unit._type = XGraphicsUnit.Centimeter;
                break;

            case "in":
                unit._type = XGraphicsUnit.Inch;
                break;

            case "mm":
                unit._type = XGraphicsUnit.Millimeter;
                break;

            case "":
            case "pt":
                unit._type = XGraphicsUnit.Point;
                break;

            case "pu": // presentation units
                unit._type = XGraphicsUnit.Presentation;
                break;

            default:
                throw new ArgumentException ("Unknown unit type: '" + typeStr + "'");
        }

        return unit;
    }

    /// <summary>
    /// Converts an int to an XUnit object with type set to point.
    /// </summary>
    public static implicit operator XUnit (int value)
    {
        XUnit unit;
        unit._value = value;
        unit._type = XGraphicsUnit.Point;
        return unit;
    }

    /// <summary>
    /// Converts a double to an XUnit object with type set to point.
    /// </summary>
    public static implicit operator XUnit (double value)
    {
        XUnit unit;
        unit._value = value;
        unit._type = XGraphicsUnit.Point;
        return unit;
    }

    /// <summary>
    /// Returns a double value as point.
    /// </summary>
    public static implicit operator double (XUnit value)
    {
        return value.Point;
    }

    /// <summary>
    /// Memberwise comparison. To compare by value,
    /// use code like Math.Abs(a.Pt - b.Pt) &lt; 1e-5.
    /// </summary>
    public static bool operator == (XUnit value1, XUnit value2)
    {
        // ReSharper disable CompareOfFloatsByEqualityOperator
        return value1._type == value2._type && value1._value == value2._value;

        // ReSharper restore CompareOfFloatsByEqualityOperator
    }

    /// <summary>
    /// Memberwise comparison. To compare by value,
    /// use code like Math.Abs(a.Pt - b.Pt) &lt; 1e-5.
    /// </summary>
    public static bool operator != (XUnit value1, XUnit value2)
    {
        return !(value1 == value2);
    }

    /// <summary>
    /// Calls base class Equals.
    /// </summary>
    public override bool Equals (object? obj)
    {
        return obj is XUnit unit && this == unit;
    }

    /// <summary>
    /// Returns the hash code for this instance.
    /// </summary>
    public override int GetHashCode()
    {
        // ReSharper disable NonReadonlyFieldInGetHashCode
        return _value.GetHashCode() ^ _type.GetHashCode();

        // ReSharper restore NonReadonlyFieldInGetHashCode
    }

    /// <summary>
    /// This member is intended to be used by XmlDomainObjectReader only.
    /// </summary>
    public static XUnit Parse (string value)
    {
        XUnit unit = value;
        return unit;
    }

    /// <summary>
    /// Converts an existing object from one unit into another unit type.
    /// </summary>
    public void ConvertType (XGraphicsUnit type)
    {
        if (_type == type)
        {
            return;
        }

        switch (type)
        {
            case XGraphicsUnit.Point:
                _value = Point;
                _type = XGraphicsUnit.Point;
                break;

            case XGraphicsUnit.Inch:
                _value = Inch;
                _type = XGraphicsUnit.Inch;
                break;

            case XGraphicsUnit.Centimeter:
                _value = Centimeter;
                _type = XGraphicsUnit.Centimeter;
                break;

            case XGraphicsUnit.Millimeter:
                _value = Millimeter;
                _type = XGraphicsUnit.Millimeter;
                break;

            case XGraphicsUnit.Presentation:
                _value = Presentation;
                _type = XGraphicsUnit.Presentation;
                break;

            default:
                throw new ArgumentException ($"Unknown unit type: '{type}'");
        }
    }

    /// <summary>
    /// Represents a unit with all values zero.
    /// </summary>
    public static readonly XUnit Zero = new XUnit();

    double _value;
    XGraphicsUnit _type;

    /// <summary>
    /// Gets the DebuggerDisplayAttribute text.
    /// </summary>
    /// <value>The debugger display.</value>

    // ReSharper disable UnusedMember.Local
    string DebuggerDisplay

        // ReSharper restore UnusedMember.Local
    {
        get
        {
            const string format = Config.SignificantFigures10;
            return string.Format (CultureInfo.InvariantCulture, "unit=({0:" + format + "} {1})", _value, GetSuffix());
        }
    }
}
